package com.demo.uid.config;

import com.demo.uid.constant.ConfigConstant;
import com.demo.uid.service.SnowFlakeService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;

/**
 * 自动配置机器ID
 */
@Component
@Configuration
@Slf4j
public class MachineIdConfig implements InitializingBean {

    /**
     * 机器id
     */
    private Integer machineId;
    /**
     * 本地ip地址
     */
    private String localIp;

    /**
     * StringRedisTemplate 的默认序列化方式为 StringRedisSerializer
     */
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private SnowFlakeService snowFlakeService;

    /**
     * hash机器IP初始化一个机器ID
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        // 192.168.0.233
        localIp = getIPAddress();
        // 本地ip地址 转换  1921680233
        Long ip = Long.parseLong(localIp.replaceAll("\\.", ""));
        // 机器id 取模 0-31
        machineId = ip.hashCode() % 32;
        // 创建一个机器ID
        createMachineId();
        log.info("初始化 machine_id :{}", machineId);
        snowFlakeService.init(machineId);
    }

    /**
     * 获取ip地址
     *
     * @return
     * @throws UnknownHostException
     */
    private String getIPAddress() throws UnknownHostException {
        InetAddress address = InetAddress.getLocalHost();
        return address.getHostAddress();
    }

    /**
     * 主方法：首先获取机器 IP 并 % 32 得到 0-31
     * 使用 机器id + IP 作为 Redis 的 key，机器IP作为 value，存储到Redis中
     *
     * @return
     */
    private Integer createMachineId() {
        try {
            // 向redis注册，并设置超时时间
            log.info("注册一个机器ID到Redis, 机器id：{}, IP:{}", machineId ,localIp);
            Boolean flag = registerMachine( machineId, localIp);
            // 注册成功
            if (flag) {
                // 启动一个线程更新超时时间
                updateExpTimeThread();
                // 返回机器Id
                log.info("Redis中端口没有冲突 机器id：{}，IP：{}", machineId, localIp);
                return machineId;
            }
            // 注册失败，可能原因 Hash%32 的结果冲突
            if (!checkIfCanRegister()) {
                // 如果 0-31 已经用完，使用 32-64之间随机的ID
                getRandomMachineId();
                createMachineId();
            } else {
                // 如果存在剩余的ID
                log.warn("Redis中端口冲突了，使用 0-31 之间未占用的机器Id：{}，IP：{} ", machineId, localIp);
                createMachineId();
            }
        } catch (Exception e) {
            // 获取 32 - 63 之间的随机Id
            // 返回机器Id
            log.error("Redis连接异常,不能正确注册雪花机器号:{}, IP:{} ", machineId , localIp, e);
            log.warn("使用临时方案，获取 32 - 63 之间的随机数作为机器号，请及时检查Redis连接");
            getRandomMachineId();
            return machineId;
        }
        return machineId;
    }

    /**
     * 1.注册机器
     * 2.设置超时时间
     *
     * @param machineId 取值为0~31
     * @return
     */
    private Boolean registerMachine(Integer machineId, String localIp) {
        // key 数据中心ID + 机器ID value 机器IP
        String key = String.valueOf(ConfigConstant.dataCenterId + machineId);
        Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(key,localIp);

        if (absent) {
            // 过期时间 1 天
            stringRedisTemplate.expire(key, 1, TimeUnit.DAYS);
            return true;
        } else {
            // 如果Key存在，判断Value和当前IP是否一致，一致则返回True
            String value = stringRedisTemplate.opsForValue().get(key);
            if (StringUtils.equals(localIp, value)) {
                // IP一致，注册机器ID成功
                stringRedisTemplate.expire(key, 1, TimeUnit.DAYS);
                return true;
            }
            return false;
        }
    }

    /**
     * 1.更新超時時間
     * 注意，更新前检查是否存在机器ip占用情况
     */
    private void updateExpTimeThread() {
        // 开启一个线程执行定时任务:
        // 每23小时更新一次超时时间
        new Timer(localIp).schedule(new TimerTask() {
            @Override
            public void run() {
                // 检查缓存中的ip与本机ip是否一致, 一致则更新时间，不一致则重新获取一个机器id
                Boolean b = checkIsLocalIp(machineId);
                if (b) {
                    log.info("IP一致，更新超时时间 ip:{},machineId:{}, time:{}", localIp, machineId, new Date());
                    stringRedisTemplate.expire(String.valueOf(ConfigConstant.dataCenterId + machineId), 1, TimeUnit.DAYS);
                } else {
                    // IP冲突
                    log.info("重新生成机器ID ip:{},machineId:{}, time:{}", localIp, machineId, new Date());
                    // 重新生成机器ID，并且更改雪花中的机器ID
                    getRandomMachineId();
                    // 重新生成并注册机器id
                    createMachineId();
                    // 更改雪花中的机器ID
                    snowFlakeService.updateSnowFlakeMachineId(machineId);
                    // 结束当前任务
                    log.info("Timer->thread->name:{}", Thread.currentThread().getName());
                    this.cancel();
                }
            }
        }, 10 * 1000, 1000 * 60 * 60 * 23);
    }

    /**
     * 检查Redis中对应Key的Value是否是本机IP
     *
     * @param machineId
     * @return
     */
    private Boolean checkIsLocalIp(Integer machineId) {
        String ip = stringRedisTemplate.opsForValue().get(String.valueOf(ConfigConstant.dataCenterId + machineId));
        log.info("checkIsLocalIp->ip:{}", ip);
        return localIp.equals(ip);
    }

    /**
     * 获取32-63随机数
     */
    private void getRandomMachineId() {
        machineId = (int) (Math.random() * 31) + 31;
    }

    /**
     * 检查是否被注册满了
     *
     * @return
     */
    private Boolean checkIfCanRegister() {
        boolean flag = true;
        // 判断0~31这个区间段的机器IP是否被占满
        for (int i = 0; i < 32; i++) {
            flag = stringRedisTemplate.hasKey(String.valueOf(ConfigConstant.dataCenterId + i));
            // 如果不存在。设置机器Id为这个不存在的数字
            if (!flag) {
                machineId = i;
                break;
            }
        }
        return !flag;
    }

    public Integer getMachineId() {
        return machineId;
    }
}
